프로젝트 최적화 실전 | 분석 및 라이트하우스 기반 로딩 성능 개선
2024. 02. 13.
#최적화
서비스
분석전 lighthouse 점수
- 모바일 환경은 CPU 와 네트워크에 제한이 들어간다
- 모바일 환경에서 CPU 와 네트워크 제한이 있기 때문에 모바일 환경을 개선하고자 한다!
그럼 자연스럽게 PC 환경도 개선 가능
- 라이트 하우스와 성능 탭을 보면 DCL 과 L (로드) 시점도 7초 이후이고
FP, FCP 와 그리고 LCP 시점도 8.5 초이다. 그리고 이미지 파일들 (네트워크에서 초록색 영역) 도 다운로드 및 로드하는데도 시간이 오래 걸리는 것을 알 수 있다!
→ 즉, 최적화 포인트가 로드 시점을 땡겨야한다. + CLS 개선
다양한 방법이 있지만, 하나씩 차근 차근 해결해보자!
1. Network 압축 적용
- 백엔드 개발자한테 요청
문제점 확인
- Lighthouse 총점이 54 이다.
Lighthouse 에서 제안한 “텍스트 압축 사용” 항목을 확인하면 전송 크기를 3.6KiB 정도 줄일 수 있다고 제안한다.
- API 요청 응답값의 사이즈가 5.1Kb 인 것을 확인할 수 있다.
개선
- 개선 방법은 HTTP 응답값을
Content-Encoding: gzip
으로 gzip 과 같이 압축되도록 해야한다.
이는 백엔드 개발자한테 부탁해서 처리했다.
- API 응답값으로 gzip 으로 압축된 것을 확인할 수 있다.
- 그리고 응답 크기가
1.4kB
로 준 것을 확인할 수 있다
개선 결과
- 총점이 54 → 56점으로 상승했다
2차 개선
추가적으로 좀 더 개선하고 싶으면 아래와 같이 처리해봤다.
// 기존 코드 export const getScrollProducts = async (pageParam: number) => { const response = await client.get( `https://pet-commerce.shop/v1/api/product?pageNumber=${pageParam}&size=2&sortBy=createdAt`, ); return response.data; };
// 개선 코드 export const getScrollProducts = async (pageParam: number) => { const response = await client.get( `https://pet-commerce.shop/v1/api/product?pageNumber=${pageParam}&size=${ pageParam === 1 ? 2 : 15 }&sortBy=createdAt`, return response.data; };
- 처리한 내용은 첫번째 페이지 요청 (pageParam === 1) 인 경우 2개만 요청하고 그 이후부터는 15개를 요청했다.
(이후 에 4개만 요청하는 걸로 변경함)
⇒ FP 와 LCP 를 먼저 처리할 수 있다고 판단하여 이와 같이 처리했다.
2차 개선 결과
- 총점이 56 → 58 점으로 상승했고
네트워크를 보면 맨 처음 요청이 773B 로 줄인 것을 확인할 수 있다.
2. Layout Shift 개선하기 (CLS 개선)
문제점 확인
- 해당 Layout Shift 가 발생한 것을 확인했다.
개선
- 스켈레톤 UI 를 만들어서 API 요청 중이면 (loading) Skeleton UI 가 나오게 하여 Layout Shift 가 발생하는 것을 방지했다.
개선 결과
- 총점 58 → 62 점으로 개선되었고
Cumulative Layout Shift 도 0.124 → 0 으로 개선되었다.
3. 폰트 최적화하기
문제점 확인
- 폰트를 다운받는데 19.55s 나 걸리는 것을 확인할 수 있다.
- 리액트 개발 환경이라서 SPA 파일이
index-94823a4c.js
인287kB
보다 폰트의 크기가 약 2배 정도 (590 kB) 큰 것을 알 수 있다!
개선하기
개선하기 폰트 최적화 포인트에 대해 알아보자
- 폰트 convert 사이트 에 접속하여 ttf 폰트 파일을 → woff2 로 변환
- ttf 파일이 590KB → 177KB 로 용량이 줄었다.
@font-face { font-family: 'SUIT'; src: url('/fonts/SUIT-Bold.woff2') format('woff2'), url('/fonts/SUIT-Bold.woff') format('woff'), url('/fonts/SUIT-Bold.ttf') format('truetype'); font-display: optional; }
개선 결과
- 전체 점수가 64점 → 72점
Lagest Contentful Paint 가 7.7s → 5.7s 로 개선
- 폰트 크기도 590kB → 177 kB 로 줄어들었다.
폰트 최적화 더 하고 싶다면 참고
4. 필수 원본 미리 연결하기
문제점 확인
- 우리 서비스에서 이미지를 불러오는 주소를 미리 연결해서 리소스 다운 시간을 줄이고자 한다
개선하기
- 추가하기
개선 결과
- 전체 점수가 72점 → 76점
- Lagest Contentful Paint 가 6.3s → 5.7s 로 개선
5. 이미지 파일 개선하기
문제점 확인
- 네트워크탭을 보면 원본 이미지 사이즈인 93.4kB, 89.9kB 등으로 나오는 것을 확인할 수 있다.
- 라이트하우스가 이미지 파일 관련해서 개선시킬 수 있는 방안을 제안했고
성능을 확인해보니, 이미지를 다운받고 로드하는데 오래걸리는 것을 알 수 있다.
⇒ 이로 인해 LCP 가 오래 걸린다고 판단한다.
개선
- https://squoosh.app/ 사이트에서 해당 이미지를 사이즈를 줄이고 각 이미지를 배포한 해당 이미지로 대체
- 원래는 이미지 업로드 방식을 개선해야하는데 (클라우디너리를 이용해서 CDN + 이미지 크롭 및 화질 선택하는 등) 이번에는 테스트 하는 이미지만 업데이트해서 변경하는 형태로 진행함!
개선 결과
- 총점수도 증가하고
네트워크 이미지 파일도 줄어든 것을 확인할 수 있다.
추가로 스와이퍼 첫번째 이미지를 가져올때 오래동안 요청해서 해당 부분 개선하기!
문제 분석
- swiper 이미지가 엄청나게 149ms → 349ms 만큼 오래걸리는 것을 확인할 수 있다.
우리 서비스 SPA 로 빌드된
index-*.js
파일보다 더 오래 걸리는 것을 확인할 수 있다.→ 의심가는 것은 파일 용량보다 i.postImg.cc 사이트에서 응답이 오래걸린다고 파악된다.
→ 그리고 네트워크 시간을 조작하지 않아서 그렇지, 라이트하우스의 모바일용으로 하면 엄청 오래걸릴것이다!
개선
- swiper 이미지를 용량도 좀 줄이고
i.postimg.cc
사이트가 아닌 우리 서비스를 제공하는 cloudinary 사이트레 이미지를 올리고 해당 URL 을 바라보게 했다.
개선 결과
- 208.47ms → 16.65ms 로 줄인것을 확인할 수 있다.
- 총점도 78 → 82 점으로 올랐다
추가로 이미지 url 이 달라졌기에 “4에서 처리한 필수 원본 미리 연결하기”도 변경해야함
<!doctype html> <html lang="ko"> <head> <meta charset="UTF-8" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>CLIP</title> <link rel="preload" href="/fonts/SUIT-Bold.woff2" as="font" type="font/woff2" crossorigin> <link rel="preconnect" href="https://res.cloudinary.com" /> <link rel="preconnect" href="https://pet-commerce.shop" /> <link rel="preload" as="image" href="https://res.cloudinary.com/du4w00gvm/image/upload/v1704560751/swiper1_tshxjo.webp" type="image/webp" /> </head> <body> <div id="root"></div> <script type="module" src="/src/main.tsx"></script> </body> </html>
마지막으로 “이미지크기 적절하게 설정하기” 를 개선해보자
문제 분석
- 라이트하우스에서 이미지 크기 조절을 권장해서 확인해보니
사이트에서 보여주는 크기(렌더링된 크기) 는
169 x 205 px
인데 실제 크기는 476 x 651px
이므로 개선해보자!개선
export const getScrollProducts = async (pageParam: number) => { const response = await client.get( `https://pet-commerce.shop/v1/api/product?pageNumber=${pageParam}&size=${ pageParam === 1 ? 4 : 15 }&sortBy=createdAt`, ); // 아래 코드 추가 const data = response.data.map((item: any) => { const imageUrl = item.imageUrl as any[]; const thumbnail = imageUrl[0]; const covertThumbnail = thumbnail.split('/upload').join('/upload/w_169,h_205'); // GYU-TODO: 레티나까지 고려하면 169 * 2, 205 * 2 인 w_338, h_410 으로 해야함! return { ...item, imageUrl: [covertThumbnail, ...item.imageUrl.slice(1)] }; }); return data; };
- 이미지 관리를 cloudinary 를 이용해서 했기 때문에 url 을 조작해서 가로, 세로 크기를 조절 할 수 있다.
cloudinary 에서 제공하는 기능
https://res.cloudinary.com/demo/image/upload/turtles.jpg 해당 이미지가 있으면
/upload
뒤에 여러 가지 속성을 줘서 이미지 정보를 조절 할 수 있다! 개선 결과
- 총점 82 → 84점 상승했고, 해당 항목이 사라진것을 확인할 수 있다.
- 현재는 렌더링된 크기와 맞게 딱 맞춰서 168, 205 로 설정했는데 레티나까지 고려하면 169 * 2, 205 * 2 인 w_338, h_410 으로 해야한다.
6. 사용하지 않는 JS 파일 줄이기
문제 분석
- 리액트 프로젝트이기 때문에 SPA 방식으로 동작해서 js 파일이 큰 것을 알 수 있다!
해당 리소스를 줄이는 방식을 통해 개선해보자!
- 그리고 빌드된 결과물을 보더라도 875.79kB 로 용량이 큰 것임을 알 수 있다. (압축해도 286.68kB)
개선
- 우선 vite-bundle-visualizer 를 이용해서 분석해보니 빌드된 js 파일을 줄이기 위한 여러가지 요소가 보였다.
- react-query 와 @tanstack/react-query 를 혼용해서 사용하고 있엇다.
⇒ react-query 만 사용하도록 변경
- pages 에 여러 페이지들이 최초 로드시에 모든 페이지가 다 포함되어 있었음
⇒ 첫 랜딩 페이지인 home 페이지를 제외하고 lazy 로드를 통해 용량 줄이기
⇒ 24개의 페이지가 있는 시간상 12개 정도만 lazy 로드를 했다. → 더 줄일 수 있는 방안 존재함을 의미
- icon 들을 관리하는데 해당 icon 모아둔 것도 용량이 컸다.
⇒ 아이콘 관리 방식을 개선 했다고 가정
- swiper 라이브러리도 생각보다 컸다.
서비스에서 사용하는 것은 간단한데 해당 라이브러리가 서비스에 적용된 기능에 비해 컸다.
이는 캐러셀 기능을 직접구현해서 처리할 수 있다. → 하지만 이번에는 하지 않음!
→ 그래서 간단하게 1~3 번까지만 적용하고 확인해보자!
개선 결과
- vite-bundle-visualizer 를 확인하면 icon 도 많이 줄었고,
react-query 만 존재하고
각 components, page 들을 잘게 잘게 분리됐음을 알 수 있다.
- 빌드된 결과물을 확인하면
875.79kB → 616.95kB 로 줄었고, 각각 잘 분리됐음을 알 수 있다
- 총점도 82 → 86점으로 상승했다.
pages lazy 적용시 주의할 점
import { lazy, Suspense } from 'react'; import { createBrowserRouter } from 'react-router-dom'; const Menu = lazy(() => import('@/pages/Category/Menu')); const Coupon = lazy(() => import('@/pages/Coupon/Coupon')); const DetailPage = lazy(() => import('@/pages/DetailPage/DetailPage')); import Home from '@/pages/Home/Home.tsx'; export const router = createBrowserRouter([ { path: '/', element: ( <Suspense fallback={<div>loading...</div>}> <DefaultLayout /> </Suspense> ), // errorElement: <div>Not Found Page</div>, children: [ // 제품 및 검색 { index: true, element: <Home />, }, { path: '/product/:productId', element: <DetailPage />, }, { path: '/category', element: <Menu />, }, // ... { path: '/mypage/wish', element: ( <ProtectedRoute onlyBuyer> <WishPage /> </ProtectedRoute> ), }, ], }, ]);
- 위와 같이
lazy()
함수로 페이지를 동적으로 가져온다.
- lazy 로 가져온 페이지들을 suspense 로 감싸서 가져온다.
각 라우팅마다 suspense 로 개별적으로 감싸도 되지만 이번에는 상위 라우터에서 suspense로 래핑했다.
⇒ suspense 로 인해 화면이 깜빡이는 요소가 있는데 이를 UX 와 성능 측면에서 트레이드 오프를 판단 후 적용하면 좋다!
여기까지 라이트하우스 점수 54 → 87점까지 개선해보았다!
⇒ js 파일을 더 줄일수도 있고 , 다른 최적화 포인트도 남아있기 때문에 성능을 더 개선시킬 포인트가 존재한다!
- 현재 최적화 포인트는 로딩 성능을 테스트 하는 것으로 빠르게 로딩처리를 하고 있다.
폰트와 SPA 를 수행하기 위한 js 파일을 개선하면 더 빠르게 로드되어 사이트를 개선할 수 있을거 같다!
- 폰트를 개선하기 위한 개선 포인트는 ttf →woff2 로 폰트 사이즈를 줄이긴 했지만, 우리 사이트는 영어와 한글을 사용하는데 한글 폰트는 일반적인 상황에서 거의 사용하지 않는 글자(쀍 꺊 등)도 포함되었기 때문에 폰트 subset 으로 자주 사용하는 글자만 포함시키면 더 줄일 수 있을거 같다!
- SPA 를 수행하기 위해 JS 파일도 더 개선하면 더 빠르게 로드할 수 있다고 판단한다!
참고로 최적화 수행 환경은 네트워크 → 캐시 사용 중지 인 상태였다.
근데 우리가 최적화할 때 “4. 필수 원본 미리 연결하기” 등 캐시 걸려 있으면 더 좋은 최적화 포인토 있었기 때문에 캐시를 사용한다면 더 빠른 로드를 확인할 수 있다.
해당 내용은 여기서 마무리하는데 디테일하게 남은 작업과 라이트하우스에서 제안한 개선점을 수행하면 된다! 끝!!